#!/usr/bin/python

#
# Copyright (c) 2010 Alexey Michurin <a.michurin@gmail.com>,
#                    Mihail Razuvaev (goglus) <goglus@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

####################################################


VERSION = '0.5.8'


################ GENERIC SKIN ######################

DEFAULT_CONFIG = r'''
[generic]
animation_speed: 160
width: 124
height: 18
background: #555555
position_0_x: 2
position_0_y: 2
position_1_x: 20
position_1_y: 2
position_2_x: 46
position_2_y: 2
position_3_x: 64
position_3_y: 2
position_4_x: 90
position_4_y: 2
position_5_x: 108
position_5_y: 2
digit_common_head:
  zoom=2
  delay=1
  .=#555555

digit_0_hint:
  W=#ffffff
  .WWWWW.
  W.....W
  W...W.W
  W..W..W
  W.W...W
  W.....W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_0_sign:
  W=#ffffff
  .W...W.
  .WWWWW.
  W..W..W
  .WWWWW.
  .WW.WW.
  .WWWWW.
  WW.W.WW
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_1_hint:
  W=#ffffff
  ..WW...
  ...W...
  ...W...
  ...W...
  ...W...
  ...W...
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_1_sign:
  W=#ffffff
  WW...WW
  WWWWWWW
  W..W..W
  WWWWWWW
  W..W..W
  ..WWW..
  WWWWWWW
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_2_hint:
  W=#ffffff
  .WWWWW.
  W.....W
  ......W
  ......W
  .WWWWW.
  W......
  WWWWWWW
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_2_sign:
  W=#ffffff
  ..W.W..
  ...W...
  .WWWWW.
  WW.W.WW
  WWWWWWW
  .WW.WW.
  ..W.W..
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_3_hint:
  W=#ffffff
  .WWWWW.
  W.....W
  ......W
  ...WWW.
  ......W
  W.....W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_3_sign:
  W=#ffffff
  .WWWWW.
  W.WWW.W
  .WW.WW.
  ...W...
  W.WWW.W
  .WW.WW.
  W.WWW.W
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_4_hint:
  W=#ffffff
  ....WWW
  ...W..W
  ..W...W
  .W....W
  WWWWWWW
  ......W
  ......W
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_4_sign:
  W=#ffffff
  .W...W.
  ..WWW..
  W.W.W.W
  WWWWWWW
  ..WWW..
  WWWWWWW
  W.....W
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_5_hint:
  W=#ffffff
  WWWWWWW
  W......
  WWWWWW.
  W.....W
  ......W
  W.....W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_5_sign:
  W=#ffffff
  WWW.WWW
  ..W.W..
  ..WWW..
  .W.W.W.
  ..W.W..
  ..W.W..
  ...W...
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_6_hint:
  W=#ffffff
  .WWWWW.
  W......
  WWWWWW.
  W.....W
  W.....W
  W.....W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_6_sign:
  W=#ffffff
  ...W...
  .WW.WW.
  W.W.W.W
  WWWWWWW
  W..W..W
  WW.W.WW
  .WW.WW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_7_hint:
  W=#ffffff
  WWWWWWW
  ......W
  .....W.
  ....W..
  ...W...
  ...W...
  ..WWW..
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_7_sign:
  W=#ffffff
  .W...W.
  .WWWWW.
  W.WWW.W
  .WWWWW.
  ..WWW..
  ..W.W..
  ...W...
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_8_hint:
  W=#ffffff
  .WWWWW.
  W.....W
  W.....W
  .WWWWW.
  W.....W
  W.....W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_8_sign:
  W=#ffffff
  .WWWW..
  W.W.W..
  WWWWW.W
  ...WWWW
  WWWWW..
  .WWWW..
  ..W.W..
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_9_hint:
  W=#ffffff
  .WWWWW.
  W.....W
  W.....W
  W.....W
  .WWWWWW
  ......W
  .WWWWW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

digit_9_sign:
  W=#ffffff
  WWWWWWW
  W.WWW.W
  WWW.WWW
  .W...W.
  .WWWWW.
  ..W.W..
  .WW.WW.
  +
  W=#eeeeee
  +
  W=#dddddd
  +
  W=#cccccc

background_image:
  W=#cccccc
  ..............................................................
  ..............................................................
  ...................WW....................WW...................
  ...................WW....................WW...................
  ..............................................................
  ...................WW....................WW...................
  ...................WW....................WW...................
  ..............................................................
  ..............................................................
'''

####################################################


import time
import socket
import re
import getopt
import sys
import os
import signal
import fcntl
import tempfile
import codecs
from ConfigParser import ConfigParser
from Tkinter import Tk, Label, PhotoImage, Menu, \
                    Toplevel, Button, Frame, Canvas, \
                    LEFT, GROOVE, READABLE, EXCEPTION, \
                    NORMAL, DISABLED, NW, \
                    StringVar


################ CONSTANTS #########################


ABOUT = (  'PixiClock ' + VERSION + '\n'
           '\n'
           'Released under the terms of BSD license.\n'
           '\n'
           'Copyright (c) 2010\n'
           '   Hacked by Alexey Michurin <a.michurin@gmail.com>\n'
           '   Graphic design by Mihail Razuvaev (goglus) <goglus@gmail.com>\n'
           '\n'
           'PixiClock is tiny desktop clock widget for true geeks.\n'
           '\n'
           'Project homepage:\n'
           '   http://pixiclock.googlecode.com/\n'
           'More about pixi-culture:\n'
           '   http://goglus.com/\n'
           '   http://goglus.com/pixel/urod.htm'
        )


HELP =  (  'Command line options.\n'
           '\n'
           '-v\n'
           '    print version\n'
           '-h\n'
           '    print help message\n'
           '-p PORT\n'
           '    run pixiclock in network mode on PORT; try\n'
           '    $ pixiclock-clietn -p PORT "TEST"\n'
           '-n\n'
           '    same as -p 7070\n'
           '-a HH:MM\n'
           '    set up alarm; can be used many times\n'
           '-a HH:MM@/path/command\n'
           '    set up alarm and command to execute on it\n'
           '    (there is no way to pass arguments to command yet)\n'
           '-f FILE\n'
           '    load skin from configuration file;\n'
           '    can be used many times to load diferent\n'
           '    skins in conjunction\n'
           '-c COMMAND\n'
           '    piped watchdog command\n'
           '-w\n'
           '    do not ignore window manager\n'
           '-g GEOMETRY\n'
           '    geometry of main window in X-format.\n'
           '-d\n'
           '    daemonize'
        )


################ IPC FUNCTIONALITY #################
# Commands:
# BG=#000000
# FG=#000000
# BD=#000000
# BDWIDTH=2
# GEOMETRY=-10-10
# MARGIN=4
# DELAY=1000


def open_socket(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setblocking(0)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
           sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) | 1)
    sock.bind(('127.0.0.1', port))
    sock.listen(100)
    set_close_on_exec(sock)
    return sock


def set_close_on_exec(sock):
    # the subprocess shouldn't have the listening socket open and other
    fd = sock.fileno()
    flags = fcntl.fcntl(fd, fcntl.F_GETFD)
    flags |= fcntl.FD_CLOEXEC
    fcntl.fcntl(fd, fcntl.F_SETFD, flags)


class channel:

    def __init__(o, w, sock):
        o.window = w
        o.buffer = ''
        o.window.tk.createfilehandler(sock, READABLE | EXCEPTION , o)
        o.window.connections.append(sock)

    def __call__(o, s, m):
        if (m & EXCEPTION) == 0:
            d = s.recv(4096)
        else:
            d = ''
        if d == '':
            o.window.tk.deletefilehandler(s)
            o.window.connections.remove(s)
            s.close()
            o.window.accept_data(o.buffer)
        else:
            o.buffer += d


class io_proxy:

    def __init__(o, r, fd):
        o.root = r
        o.window = r.window
        o.fd = fd
        o.td = None
        o.buffer = ''
        o.window.tk.createfilehandler(fd, READABLE | EXCEPTION, o)

    def __call__(o, s, m):
        if (m & EXCEPTION) == 0:
            # Note: since you don't know how many bytes are available
            # for reading, you can't use the Python file object's read
            # or readline methods, since these will insist on reading
            # a predefined number of bytes.
            d = os.read(s, 4096)
        else:
            d = ''
        if len(d) == 0:
            o.root.revival()
            return
        o.buffer += d
        # replan data emition
        # we emit data if subprocess keep silence during 1/2 second
        if not o.td is None:
            o.window.after_cancel(o.td)
        o.td = o.window.after(500, o.emit_data)

    def emit_data(o):
        # we must check is string valid UTF-8
        # otherwise Tk-font-engine may break
        try:
            codecs.getdecoder('UTF-8')(o.buffer)
        except UnicodeDecodeError:
            o.buffer = 'Sorry. Data not valid UTF-8 string.'
        # BUT:
        #   It is not complete solution!
        #   For example the string '\xe2\x80\x8f' UTF
        #   is valid (0xE = 1110b: start of 3-byte sequence),
        #   but it breaks Tk library with message:
        # X Error of failed request:  BadValue (integer parameter \
        # out of range for operation)
        #   Major opcode of failed request:  45 (X_OpenFont)
        #   Value in failed request:  0x2a00047
        #   Serial number of failed request:  745
        #   Current serial number in output stream:  746
        # It affects to Georgian alphabet, Indian, Chinese,
        # Korean, Japanese and may be other alphabets
        # Is it bug in Tk?
        o.window.accept_data(o.buffer)
        o.buffer = ''
        o.td = None

    def close(o):
        try:
            os.close(o.fd)
        except OSError: # pass if 'Bad file descriptor'
            pass
        o.window.tk.deletefilehandler(o.fd)


class proc_manager:

    def __init__(o, w, proc):
        o.window = w # used by io_proxy
        o.process = proc
        o.pid = None
        o.stdout = None
        o.up()

    def up(o):
        r, w = os.pipe()
        pid = os.fork()
        if pid:  # this is the parent process
            os.close(w)
            o.pid = pid
            o.stdout = io_proxy(o, r)
            # we can proxy stderr in the same way
            # who need?
        else:    # in the child
            os.close(r)
            os.dup2(w, 1) # stdout
            # the subprocess shouldn't have the listening
            # socket open and other, but we care about it before
            # see set_close_on_exec()
            os.execv(o.process, ())

    def die(o):
        o.stdout.close()
        try:
            os.kill(o.pid, signal.SIGKILL)
        except OSError:
            pass
        o.pid = None

    def revival(o):
        o.die()
        o.window.accept_data('BG=#CC0000\n'
                             'FG=#FFFFFF\n'
                             'BD=#FFFFFF\n'
                             'BDWIDTH=1\n'
                             'MARGIN=7\n'
                             'GEOMETRY=-10-100\n'
                             'DELAY=10000\n'
                             'INTERNAL ERROR:\n'
                             'Process\n' + o.process + '\ndies.\n'
                             'Will be revival in a minute.')
        o.window.after(60000, o.up)


class net_message(Toplevel):

    def __init__(o, r, sock, subprocs):
        Toplevel.__init__(o, r)
        o.withdraw()
        o.overrideredirect(True)
        o.title('pixiclock message')
        o.label = Label(o,
                        bg='#444444',
                        fg='#cccccc',
                        justify=LEFT,
                        highlightthickness=0,
                        borderwidth=0,
                        font=('Helvetica', 10))
        o.label.pack()
        o.sock = sock
        o.connections = [] # used by channel
        if sock:
            o.tk.createfilehandler(sock, READABLE, o.accept_connection)
        o.subprocs = []
        for proc in subprocs:
            o.subprocs.append(proc_manager(o, proc))
        o.prog_handler = None
        o.prog = []
        o.prog_re = re.compile('(BG|FG|BD|BDWIDTH|MARGIN|GEOMETRY'
                               '|DELAY|PROLONGATE)'
                               '\s*[=:]\s*'
                               '([-+#a-zA-Z0-9]+);?')
        o.bind('<1>', o.abort_prog)
        o.bind('<3>', o.abort_all_prog)

    def accept_connection(o, s, m):
        sock, addr = s.accept()
        set_close_on_exec(sock)
        channel(o, sock)

    def accept_data(o, data):
        pos = 0
        prog = []
        mess = ''
        pre_defaults = {'GEOMETRY': '-10-10',
                        'BG': '#444444',
                        'FG': '#cccccc',
                        'BD': '#000000',
                        'BDWIDTH': 1,
                        'MARGIN': 3}
        delay_found = False
        while True:
            m = o.prog_re.search(data, pos)
            if m is None:
                mess += data[pos:]
                break
            c = m.group(1)
            p = m.group(2)
            delay_found = delay_found or c == 'DELAY'
            if not delay_found:
                try:
                    del pre_defaults[c]
                except KeyError:
                    pass
            prog.append((c, p))
            mess += data[pos:m.start()]
            pos = m.end()
        # we add commands in this order:
        # - text (purified from control sequences; should always be the first)
        # - default action
        # - actions specified in the message
        # - delay (if not specified; should always be the last)
        o.prog.extend([('TEXT', mess.strip())] +
                      pre_defaults.items() +
                      prog)
        if not delay_found:
            o.prog.append(('DELAY', 5000))
        # start program
        if o.prog_handler is None:
            # we will show window later, on first DELAY
            o.exec_prog_step()

    def exec_prog_step(o):
        try:
            while True:
                if len(o.prog) == 0:
                    o.withdraw()
                    o.prog_handler = None
                    return
                c, p = o.prog[0]
                o.prog = o.prog[1:]
                if c == 'BG':
                    o.label.config(bg=p)
                elif c == 'FG':
                    o.label.config(fg=p)
                elif c == 'BD':
                    o.label.config(highlightbackground=p)
                elif c == 'BDWIDTH':
                    o.label.config(highlightthickness=int(p))
                elif c == 'GEOMETRY':
                    o.geometry(p)
                elif c == 'MARGIN':
                    o.label.config(borderwidth=int(p))
                elif c == 'DELAY':
                    if o.prog_handler is None:
                        o.deiconify()
                        o.lift()
                    o.prog_handler = o.after(int(p), o.exec_prog_step)
                    break
                elif c == 'TEXT':
                    o.label.config(text=p)
                else:
                    raise Exception('Command %s?', c)
        except Exception, e:
            err = 'ERROR %s=%s: %s' % (c, p, str(e))
            print err
            o.stop_prog()
            o.prog = [('TEXT', str(err)),
                      ('GEOMETRY', '-10-100'),
                      ('BG', '#990000'),
                      ('FG', '#ffffff'),
                      ('BD', '#ffff00'),
                      ('BDWIDTH', 5),
                      ('MARGIN', 5),
                      ('DELAY', 10000)]
            # spec. case: we leave o.prog_handler = None
            o.after(0, o.exec_prog_step)

    def stop_prog(o):
        # nothing to abort -- return
        if o.prog_handler is None:
            return
        # terminate execution
        o.after_cancel(o.prog_handler)
        o.prog_handler = None
        o.withdraw()

    def abort_all_prog(o, e):
        o.stop_prog()
        o.prog = []

    def abort_prog(o, e):
        o.stop_prog()
        # remove all commands to the nearest TEXT
        while o.prog:
            if o.prog[0][0] == 'TEXT':
                break
            o.prog = o.prog[1:]
        # If we still have commands, then resume execution.
        if o.prog:
            o.exec_prog_step()

    def vanish_connections(o):
        if not o.sock is None:
            o.sock.close()
        for c in o.connections:
            c.close()

    def vanish_subprocs(o):
        for p in o.subprocs:
            p.die()

    def report(o):
        # prepare ascii report for UI
        l = 'Subprocesses (-c):'
        if o.subprocs:
            for s in o.subprocs:
                pid, pn = s.pid, s.process
                if pid is None:
                    pid = '-'
                    pn = '[' + pn + ']'
                l += '\n   %s\t%s' % (pid, pn)
        else:
            l += '\n   [none]'
        if o.sock is None:
            l += '\nNetwork disabled.'
        else:
            l += '\nListen on (-n, -p):\n   %s:%d' % o.sock.getsockname()
            if o.connections:
                 l += '\nActive connections:'
                 for c in o.connections:
                     l += '\n   %s:%d <-> %s:%d' % (c.getsockname() + c.getpeername())
            else:
                 l += '\nNo one connected yet.'
        return l


################ X-CANVAS ##########################
# view control: (before image)
#   C = #XXXXXX
#   zoom = N
#   delay = N
# image description:
#   Cc
#   cC
# flow control: (after image)
#   + jump
#   +


class sprite_essence:

    def __init__(o, data):
        # o.prog unit:
        # 0 - PhotoImage object
        # 1 - delay
        # 2 - next frame (out fo range -- done)
        o.prog = []
        # prot - program prototype (preprocessed data)
        # 0 -- array of lines
        # 1 -- palette
        # 2 -- zoom
        # 3 -- delay
        # 4 -- jump (0 -- stop)
        prot = []
        accum = []
        pal = {}
        zoom = 1
        delay = 10
        drop = False
        frame = False
        for l in data.splitlines():
            for c in '\x20\t':
                l = l.replace(c, '')
            if len(l) == 0:
                continue
            if l[0] == '#':
                continue
            if l[0] == '+':
                frame = False
                n = 1
                if len(l) > 1:
                    n = int(l[1:])
                prot.append((accum[:], pal.copy(), zoom, delay, n))
                drop = True
                continue
            frame = True
            if l[:5] == 'zoom=':
                zoom = int(l[5:])
                continue
            if l[:6] == 'delay=':
                delay = int(l[6:])
                continue
            if l[1:2] == '=':
                pal[l[0]] = ''.join(map(lambda x: chr(int(l[x:x+2], 16)),
                                        range(3, 9, 2)))
                continue
            if drop:
                drop = False
                accum = []
            accum.append(l)
        if frame: # append last frame if any
            prot.append((accum, pal, zoom, delay, 0))
        # prot ready; convert to o.prog
        for accum, pal, zoom, delay, jump in prot:
            data = 'P6\n# PixiClock\n%d %d\n255\n%s' % (
                             len(accum[0]),
                             len(accum),
                             ''.join(map(lambda x: pal[x], ''.join(accum))))
            #
            # Create a PhotoImage
            # For me yet unidentified reasons the following fail:
            #   image = Tkinter.PhotoImage(data=data)
            # Error with Python:
            #   _tkinter.TclError: truncated PPM data
            # OsX: distorted images (???)
            #
            # For example, these data correctly loaded from the file,
            # but can not be downloaded directly.
            #   'P6\n1 1\n255\n\xd6\x8d\x95'
            # Observed, that the trouble occurs when the color value
            # greater than 0xC0. UTF?
            #
            # What I dig:
            #
            # All interaction with Tk passes through the function
            #   static PyObject * Tkapp_Call(PyObject *selfptr, PyObject *args)
            # which handles all the arguments in a unified manner,
            # through the function
            #   static Tcl_Obj* AsObj(PyObject *value)
            # This function tries to parse unicode-data and leads to
            # deterioration of binary data.
            # I see no easy way to fix this situation.
            #
            # In fact, the exception occurs in $(TK_SOURCE)/generic/tkImgPPM.c
            # But i'm sure (I checked it out myself), that tkImgPPM.c
            # is not guilty. Apparently, the problems still in _tkinter.c
            # design ($(PYTHON_SOURCE)/Modules/_tkinter.c).
            #
            # See also:
            # #define PyUnicode_Check
            # in $(PYTHON_SOURCE)/Include/unicodeobject.h
            #
            (os_id, abs_path) = tempfile.mkstemp(prefix='pixiclock-', suffix='.ppm')
            fd = open(abs_path, 'w')
            fd.write(data)
            fd.close()
            o.prog.append((PhotoImage(file = abs_path).zoom(zoom), delay, jump))
            # close and remove temporary file on disk
            # os.close is needed under windows for os.remove not to fail
            try:
                os.close(os_id)
            except:
                pass
            try:
                os.remove(abs_path)
            except:
                pass


class sprite:

    def __init__(o, m):
        o.master = m
        o.image = m.create_image((0, 0), anchor=NW)
        o.essence = None
        o.index = None
        o.delay = None
        o.done = True

    def set_essence(o, e):
        if e == o.essence:
            return
        o.essence = e
        o.delay = o.essence.prog[0][1]
        if not o.delay is None:
            o.delay -= 1 # XXX: half loop more of half less?
        o.index = 0
        o.done = False
        o.update()

    def place(o, x, y):
        o.master.coords(o.image, x, y)

    def update(o):
        o.master.itemconfigure(o.image, image=o.essence.prog[o.index][0])

    def tick(o):
        if o.done:
            return
        if o.delay == 0:
            o.index += o.essence.prog[o.index][2]
            if o.index < 0:
                o.index = 0
            if o.index < len(o.essence.prog):
                o.delay = o.essence.prog[o.index][1]
            else:
                o.done = True
                return
            o.update()
        o.delay -= 1


class xcanvas(Canvas):

    def __init__(o, m):
        Canvas.__init__(o, m, highlightthickness=0)
        o.master = m
        o.sprites = []
        o.animation_speed = 100
        o.tick_loop()

    def create_sprite(o):
        s = sprite(o)
        o.sprites.append(s)
        return s

    def set_animation_speed(o, a):
        o.animation_speed = a

    def tick_loop(o):
        for i in o.sprites:
            i.tick()
        o.after(o.animation_speed, o.tick_loop)


################ CONFIGURATION #####################


class odict: # not complete but sufficient implementation of ordered dictionary

    def __init__(o):
        o.dt = {}
        o.ks = []

    def __iter__(o):
        for i in o.ks:
            yield i, o.dt[i]

    def __setitem__(o, k, v):
        if not k in o.ks:
            o.ks.append(k)
        o.dt[k] = v

    def __getitem__(o, k):
        return o.dt[k]

    def keys(o):
        return o.ks[:]


class string_feeder: # like StringIO, but ConfigParser use readline() only

    def __init__(o, s):
        o.a = s.splitlines(True)
        o.p = -1

    def readline(o):
        try:
            o.p += 1
            return o.a[o.p]
        except IndexError:
            return ''


class skin_base:

    def __init__(o, files):
        o.def_skin = None
        o.skins_base = {}
        o.names = [] # in order as in config
        o.readfp(string_feeder(DEFAULT_CONFIG))
        o.def_skin = None
        for f in files:
            o.readfp(open(f, 'r'))
        if o.def_skin is None:
            o.def_skin = 'generic'

    def readfp(o, fp):
        conf = ConfigParser()
        # experimental hack
        conf._sections = odict()
        conf._dict = odict # python 2.6
        # /hack
        conf.readfp(fp)
        sections = conf.sections()
        if len(sections) == 0:
            raise Exception('There are no sections found '
                            'in configuration file %s' % file)
        for sect in conf.sections():
            skin = {}
            # load width, height, background
            for k, t in (('width', int),
                         ('height', int),
                         ('background', str),
                         ('animation_speed', int)):
                skin[k] = t(conf.get(sect, k))
            # load digits
            a = []
            for i in range(10):
                 pfx = 'digit_%d' % i
                 a.append(map(lambda x: sprite_essence(
                     conf.get(sect, 'digit_common_head', '') +
                     conf.get(sect, pfx + '_' + x)),
                     ('sign', 'hint')))
            skin['digits'] = a
            # load background image
            skin['background_image'] = sprite_essence(
                conf.get(sect, 'digit_common_head', '') +
                conf.get(sect, 'background_image')
            )
            # load places
            p = []
            for i in range(6):
                p.append(map(lambda x: conf.get(
                                 sect,
                                 'position_%d_%s' % (i, x)), 'xy'))
            skin['positions'] = p
            name = sect
            fix = 0
            while name in o.skins_base:
                fix += 1
                name = '%s (%d)' % (sect, fix)
            if o.def_skin is None:
                o.def_skin = name
            o.skins_base[name] = skin
            o.names.append(name)

    def get(o):
        return o.skins_base[o.def_skin]

    def get_name(o):
        return o.def_skin

    def get_names(o):
        return o.names

    def set_name(o, s):
        o.def_skin = s


################ MENU ##############################


class dialog(Toplevel):

    def __init__(o, r, u, t):
        Toplevel.__init__(o, r)
        o.title(u)
        k = Frame(o, borderwidth=0)
        k.pack(padx=3, pady=3)
        f = Frame(k, borderwidth=4, relief=GROOVE)
        f.pack(padx=1, pady=1)
        Label(f, text=t, justify=LEFT, borderwidth=4).pack()
        Button(k, text='Close', command=o.destroy, pady=0).pack(padx=1, pady=1)


class popup_menu(Menu):

    def __init__(o, r):
        Menu.__init__(o, r, tearoff=0, font=('Helvetica', 8, 'bold'))
        o.root = r
        o.skin_menu = Menu(o, tearoff=0, font=('Helvetica', 8, 'bold'))
        items = r.skins.get_names()
        s = DISABLED
        if len(items) > 1:
            s = NORMAL
            o.skin = StringVar(o, r.skins.get_name())
            for m in items:
                o.skin_menu.add_radiobutton(
                    label=m,
                    command=o.update_skin,
                    variable=o.skin,
                    value=m)
        o.add_cascade(label='Skins', menu=o.skin_menu, state=s)
        o.add_command(label='IPC info', command=o.ipc_dialog)
        o.add_command(label='Alarms', command=o.alarms_dialog)
        o.add_command(label='About', command=o.about_dialog)
        o.add_command(label='Help', command=o.help_dialog)
        o.add_command(label='Exit', command=o.root.quit)

    def about_dialog(o):
        dialog(o.root, 'About', ABOUT)

    def help_dialog(o):
        dialog(o.root, 'Help', HELP)

    def alarms_dialog(o):
        if o.root.alarms:
            a = 'List of alarms:\n\n' + '\n'.join(
                   map(
                     lambda x: x[0] + '\t' + (x[1] or '-'),
                     sorted(o.root.alarms.items(), key=lambda x: x[0])
                   )
                )
        else:
            a = 'Alarms not set.\n\nUse option -a HH:MM\nto set up alarms.'
        dialog(o.root, 'Alarms', a)

    def update_skin(o):
        o.root.skins.set_name(o.skin.get())
        o.root.load_skin()
        o.root.update_digits()

    def ipc_dialog(o):
        if o.root.ext_links is None:
            a = 'IPC not used now.\n\nUse options -n, -p, -c\nto set up IPC.'
        else:
            a = o.root.ext_links.report()
        dialog(o.root, 'IPC info', a)

################ MAIN ##############################


class window(Tk):

    def init(o, alarms, skins, links, ignore_wm):
        if ignore_wm:
            o.overrideredirect(True)
            o.bind('<1>', o.start_motion)
            o.bind('<B1-Motion>', o.continue_motion)
        o.title('pixiclock')
        o.sprites = []
        o.xcanva = xcanvas(o)
        o.xcanva.pack()
        for n in range(7):
            s = o.xcanva.create_sprite()
            o.sprites.append(s)
        o.alarms = alarms # used by o.popup
        o.alarm_mode = 0
        o.alarm_x = None
        o.alarm_y = None
        o.alarm_done = ''
        o.skins = skins # used by o.popup
        o.ext_links = links # used by o.popup only
        o.popup = popup_menu(o)
        o.digital_mode = False
        o.bind('<3>', o.popup_menu)
        o.bind('<Enter>', o.mouse_enter)
        o.load_skin()
        o.timer_loop()

    def popup_menu(o, e):
        o.popup.tk_popup(e.x_root, e.y_root, 0)

    def start_motion(o, e):
        o.lift()
        o.start_motion_x = e.x_root - o.winfo_x()
        o.start_motion_y = e.y_root - o.winfo_y()

    def continue_motion(o, e):
        x = e.x_root - o.start_motion_x
        y = e.y_root - o.start_motion_y
        if -10 < x < 10:
            x = 0
        d = o.winfo_screenwidth() - o.winfo_width()
        if -10 < x - d < 10:
            x = d
        if -10 < y < 10:
            y = 0
        d = o.winfo_screenheight() - o.winfo_height()
        if -10 < y - d < 10:
            y = d
        o.geometry('+%d+%d' % (x, y))

    def mouse_enter(o, e):
        if not o.digital_mode:
            o.lift()
            o.digital_mode = True
            o.update_digits()
            o.after(2000, o.undigital_mode)

    def undigital_mode(o):
        o.digital_mode = False
        o.update_digits()

    def load_skin(o):
        skin = o.skins.get()
        o.xcanva.set_animation_speed(skin['animation_speed'])
        o.xcanva.configure(width=skin['width'],
                           height=skin['height'],
                           bg=skin['background'])
        # position of digits
        i = 0
        for x, y in skin['positions']:
            i += 1
            o.sprites[i].place(x, y)
        # position of background [!!!] hardcoded (0, 0)
        o.sprites[0].place(0, 0)
        # we need to dispaly only background
        o.sprites[0].set_essence(skin['background_image'])
        o.update_digits()

    def update_digits(o):
        hms = time.localtime()[3:6]
        hm = '%02d:%02d' % tuple(hms[:2])
        if hm in o.alarms and hm != o.alarm_done:
            cmd = o.alarms[hm]
            o.alarm_done = hm
            if cmd is None:
                o.start_alarm()
            else:
                try:
                    pid = os.fork()
                    if pid <= 0:
                        os.execv(cmd, ())
                except OSError, e:
                    sys.stderr.write('fork #1 failed: %d (%s)' % (e.errno, e.strerror))
                    sys.exit(1)
        d = 0
        if o.digital_mode:
            d = 1
        skin = o.skins.get()
        for n, v in enumerate(hms):
            o.sprites[n*2+1].set_essence(skin['digits'][v/10][d])
            o.sprites[n*2+2].set_essence(skin['digits'][v%10][d])

    def timer_loop(o):
        o.update_digits()
        o.after(1000, o.timer_loop)

    def start_alarm(o):
        if o.alarm_mode:
            return
        o.alarm_mode = 1
        o.alarm_x = o.winfo_x()
        o.alarm_y = o.winfo_y()
        o.continue_alarm()

    def continue_alarm(o):
        o.alarm_mode += 1
        if o.alarm_mode > 50:
            o.alarm_mode = 0
            o.geometry('+%d+%d' % (o.alarm_x, o.alarm_y))
        else:
            o.lift()
            dx, dy = 5*(1-2*(o.alarm_mode%2)), 0
            if (o.alarm_mode/10)%2 == 0:
               dx, dy = dy, dx
            o.geometry('+%d+%d' % (
                  (o.winfo_screenwidth() - o.winfo_width())/2 + dx,
                  (o.winfo_screenheight() - o.winfo_height())/2 + dy)
            )
            o.after(100, o.continue_alarm)


def daemonize():
        # base on "Advanced Programming in the UNIX Environment"
        # do first fork
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError, e:
            sys.stderr.write('fork #1 failed: %d (%s)' % (e.errno, e.strerror))
            sys.exit(1)
        # decouple from parent environment
        os.chdir('/')   # prevent unmounting
        os.setsid()
        os.umask(022)
        # do second fork
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError, e:
            sys.stderr.write('fork #2 failed: %d (%s)' % (e.errno, e.strerror))
            sys.exit(1)


def main():
    # parse arguments
    try:
        show_version = False
        show_help = False
        net_port = -1
        alarms = {}
        skin_files = []
        ignore_wm = True
        daemonize_app = False
        sock = None
        subprocs = []
        geom = '-1-1'
        opts, args = getopt.getopt(sys.argv[1:], 'ndwvhp:a:f:c:g:')
        for o, v in opts:
            if o == '-p':
                if sock is None:
                    sock = open_socket(int(v))
            elif o == '-n':
                if sock is None:
                    sock = open_socket(7070)
            elif o == '-c':
                # we must use abspath, cause we change CWD in daemon mode
                subprocs.append(os.path.abspath(v))
            elif o == '-v':
                show_version = True
            elif o == '-h':
                show_help = True
            elif o == '-a':
                n = v.find('@')
                if n > 0:
                    p = v[:n].strip()
                    c = v[n+1:].strip()
                else:
                    p = v.strip()
                    c = None
                if len(p) == 4:
                    p = '0' + p
                if not re.match(r'^[0-2]\d:[0-5]\d$', p):
                    raise Exception(
                          'Incorrect time format %s. Must be HH:MM.' % v)
                alarms[p] = c
            elif o == '-f':
                skin_files.append(v)
            elif o == '-w':
                ignore_wm = False
            elif o == '-d':
                daemonize_app = True
            elif o == '-g':
                geom = v
        # we must create root window before loading images
        # it is Tk specific
        r = window()
        skins = skin_base(skin_files)
        r.geometry(geom)
    except getopt.GetoptError, e:
        print str(e)
        return
    except Exception, e:
        print '%s: %s' % (str(e.__class__), str(e))
        return
    if show_version:
        print ABOUT
        return
    if show_help:
        print HELP
        return
    # daemonize
    if daemonize_app:
        daemonize()
    # this line will elect automatic child reaping. sure
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)
    os.putenv('PIXICLOCK', VERSION)
    # start neworking if confugured
    links = None
    if sock or subprocs:
        links = net_message(r, sock, subprocs)
    # init root window application
    r.init(alarms, skins, links, ignore_wm)
    # ok. main loop
    try:
        r.mainloop()
    except Exception, e:
        print '%s: %s' % (str(e.__class__), str(e))
    # close all external links
    # and try to kill all childs
    if not links is None:
        links.vanish_subprocs()
        links.vanish_connections()


if __name__ == '__main__':
    main()
